#!/usr/bin/env node
// 恢复脚本：基于 appointment_operation_log 与应用日志，尝试还原被 delete() 删除的预约记录
// 使用方法：
//   node tools/recover-appointments-from-logs.mjs --dry-run --log ../logs/app-2025-09-30.log
//   node tools/recover-appointments-from-logs.mjs --apply   --log ../logs/app-2025-09-30.log

import fs from 'node:fs'
import fsp from 'node:fs/promises'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { PrismaClient } from '@prisma/client'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

// -----------------------------
// CLI 参数解析（无第三方依赖）
// -----------------------------
function parseArgs(argv) {
  const args = {
    apply: false,
    dryRun: true,
    logPath: null,
    ids: null,
    applyNonConflicting: false,
    onlyBooked: false,
    generateNewSeq: false,
    bookedRequireRef: false,
  }
  for (let i = 2; i < argv.length; i++) {
    const a = argv[i]
    if (a === '--apply') {
      args.apply = true
      args.dryRun = false
    }
    else if (a === '--apply-nonconflicting') {
      args.apply = true
      args.dryRun = false
      args.applyNonConflicting = true
    }
    else if (a === '--dry-run') {
      args.apply = false
      args.dryRun = true
    }
    else if (a === '--only-booked') {
      args.onlyBooked = true
    }
    else if (a === '--generate-new-seq') {
      args.generateNewSeq = true
    }
    else if (a === '--booked-require-ref') {
      args.bookedRequireRef = true
    }
    else if (a === '--log') {
      const v = argv[i + 1]
      if (!v)
        throw new Error('--log 需要提供文件路径')
      args.logPath = v
      i++
    }
    else if (a === '--ids') {
      const v = argv[i + 1]
      if (!v)
        throw new Error('--ids 需要提供以逗号分隔的数字ID列表')
      args.ids = v.split(',').map(s => Number(s.trim())).filter(n => Number.isFinite(n))
      i++
    }
    else {
      // 忽略未知参数
    }
  }
  return args
}

// -----------------------------
// 日志文件解析
// -----------------------------
/**
 * 解析应用日志中的“删除预约记录成功: <sequence>”行，返回时间戳与序号的映射表
 * @param {string} logPath 日志文件路径
 * @returns {Promise<Array<{ ts: Date, sequence: string }>>} 解析结果
 */
async function parseDeletionLinesFromAppLog(logPath) {
  if (!logPath)
    return []
  const fullPath = path.isAbsolute(logPath) ? logPath : path.resolve(__dirname, '..', '..', logPath)
  if (!fs.existsSync(fullPath)) {
    console.warn(`[WARN] 指定日志文件不存在: ${fullPath}`)
    return []
  }

  const content = await fsp.readFile(fullPath, 'utf8')
  const lines = content.split(/\r?\n/)
  const results = []
  const re = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*?删除预约记录成功: (\d{8,})/ // 时间 + 序号
  for (const line of lines) {
    const m = re.exec(line)
    if (m) {
      const tsStr = m[1]
      const seq = m[2]
      const ts = new Date(tsStr.replace(' ', 'T')) // 简单解析
      if (!Number.isNaN(ts.getTime())) {
        results.push({ ts, sequence: seq })
      }
    }
  }
  return results
}

// -----------------------------
// 工具函数
// -----------------------------
function toISODate(d) {
  if (!d)
    return null
  const dd = (d instanceof Date) ? d : new Date(d)
  if (Number.isNaN(dd.getTime()))
    return null
  return dd
}

function generateSequenceNumber(now = new Date()) {
  const yyyy = String(now.getFullYear())
  const mm = String(now.getMonth() + 1).padStart(2, '0')
  const dd = String(now.getDate()).padStart(2, '0')
  const dateStr = `${yyyy}${mm}${dd}`
  const randomPart = String(Math.floor(Math.random() * 10000)).padStart(4, '0')
  return `${dateStr}${randomPart}`
}

/**
 * 在给定时间附近查找最近的删除日志序号
 * @param {Array<{ts: Date, sequence: string}>} deletions 删除记录列表
 * @param {Date} targetTs 目标时间
 * @param {number} toleranceMs 允许的时间偏差（毫秒）
 * @returns {string|null} 匹配到的序号或 null
 */
function findNearestSequenceByTs(deletions, targetTs, toleranceMs = 2000) {
  if (!deletions?.length || !targetTs)
    return null
  const t = targetTs.getTime()
  let best = null
  let bestAbs = Infinity
  for (const item of deletions) {
    const delta = Math.abs(item.ts.getTime() - t)
    if (delta < bestAbs) {
      best = item
      bestAbs = delta
    }
  }
  return bestAbs <= toleranceMs ? best.sequence : null
}

// -----------------------------
// 领域：基于操作日志重建记录
// -----------------------------
/**
 * 将 operation_data(JSON) 合并到状态
 */
function mergeOperationData(state, operationData) {
  if (!operationData)
    return state
  const data = (typeof operationData === 'string') ? safeParseJSON(operationData) : operationData
  if (!data || typeof data !== 'object')
    return state

  const merged = { ...state }
  for (const [k, v] of Object.entries(data)) {
    merged[k] = v
  }
  return merged
}

function safeParseJSON(str) {
  try {
    return JSON.parse(str)
  }
  catch {
    return null
  }
}

/**
 * 从操作日志序列（升序）构建最终预约数据快照
 * @param {Array<any>} logs 操作日志数组
 * @returns {{
 *  base: any,
 *  createdAt: Date | null,
 *  bookedAt: Date | null,
 *  status: string,
 * }} 快照结果
 */
function rebuildFromLogs(logs) {
  let base = {}
  let createdAt = null
  let bookedAt = null
  let status = 'pending'
  let errorMessage

  for (const log of logs) {
    const ts = toISODate(log.timestamp)
    switch (log.operation_type) {
      case 'create': {
        if (!createdAt)
          createdAt = ts
        base = mergeOperationData(base, log.operation_data)
        // create 输入不包含 sequence_number；is_placeholder 也不可得
        break
      }
      case 'update': {
        base = mergeOperationData(base, log.operation_data)
        if (base.appointment_status)
          status = base.appointment_status
        break
      }
      case 'book': {
        // 预约成功数据直接覆盖关键字段
        const data = safeParseJSON(log.operation_data)
        if (data) {
          base.appointment_date = data.appointment_date
          base.appointment_time = data.appointment_time
          base.timeslot_id = data.timeslot_id
          base.reference_number = data.reference_number
        }
        status = 'booked'
        bookedAt = ts
        break
      }
      case 'release': {
        status = 'cancelled'
        errorMessage = '占号已释放'
        break
      }
      case 'cancel': {
        // delete 时的日志；不改变数据状态，只用于时间映射
        break
      }
      default:
        break
    }
  }

  return { base, createdAt, bookedAt, status, errorMessage }
}

/**
 * 规范化最终待写入数据库的记录（根据 schema.prisma）
 */
function normalizeRecordForCreate(snapshot, options = {}) {
  const {
    sequenceNumber,
    id,
  } = options

  const input = snapshot.base || {}

  // 必要字段与默认值
  const record = {
    // id 可选：用于尽量复用原 ID
    ...(Number.isInteger(id) ? { id } : {}),
    sequence_number: sequenceNumber || generateSequenceNumber(),
    passport_number: input.passport_number || 'UNKNOWN_PASSPORT',
    license_number: input.license_number || 'UNKNOWN_LICENSE',
    full_name: input.full_name || '未知姓名',
    email: input.email || 'unknown@example.com',
    phone_number: input.phone_number ?? null,
    issuing_country: input.issuing_country || 'CN',
    issuing_authority: input.issuing_authority || 'GD',
    service_type: input.service_type || 'DI',
    preferred_dates: Array.isArray(input.preferred_dates) ? JSON.stringify(input.preferred_dates) : (typeof input.preferred_dates === 'string' ? input.preferred_dates : null),
    preferred_times: Array.isArray(input.preferred_times) ? JSON.stringify(input.preferred_times) : (typeof input.preferred_times === 'string' ? input.preferred_times : null),
    priority: Number.isFinite(input.priority) ? input.priority : 1,
    remarks: input.remarks ?? null,
    is_placeholder: Boolean(input.is_placeholder) === true,
    appointment_status: snapshot.status || 'pending',
    reference_number: input.reference_number ?? null,
    appointment_date: input.appointment_date ?? null,
    appointment_time: input.appointment_time ?? null,
    timeslot_id: input.timeslot_id ?? null,
    office_code: input.office_code || 'HKLO',
    batch_id: input.batch_id ?? null,
    job_id: input.job_id ?? null,
    booking_mode: input.booking_mode || 'manual',
    error_message: snapshot.errorMessage || input.error_message || null,
    raw_response: input.raw_response ?? null,
    retry_count: Number.isFinite(input.retry_count) ? input.retry_count : 0,
    created_at: snapshot.createdAt || new Date(),
    // updated_at 由数据库 @updatedAt 自动填充
    booked_at: snapshot.bookedAt || null,
    officeCode: undefined, // 保留位，避免误传多余字段
  }

  // 移除 undefined，保留 null（以符合 Prisma 可空字段）
  for (const k of Object.keys(record)) {
    if (record[k] === undefined)
      delete record[k]
  }
  return record
}

// -----------------------------
// 主流程
// -----------------------------
async function main() {
  const args = parseArgs(process.argv)
  const prisma = new PrismaClient()

  try {
    console.log(`[INFO] 恢复脚本启动. 模式=${args.dryRun ? 'dry-run' : (args.applyNonConflicting ? 'apply-nonconflicting' : 'apply')} | log=${args.logPath || '(未指定)'} | onlyBooked=${args.onlyBooked} | newSeq=${args.generateNewSeq} | requireRef=${args.bookedRequireRef}`)

    // 解析应用日志中的删除事件（用于恢复原 sequence_number）
    const appDeletionLines = await parseDeletionLinesFromAppLog(args.logPath)
    console.log(`[INFO] 应用日志中解析到 ${appDeletionLines.length} 条删除成功记录（用于时间匹配恢复序号）`)

    // 找出已被删除（通过 cancel/success/删除预约记录）的 appointment_id 列表
    const cancelLogs = await prisma.appointment_operation_log.findMany({
      where: {
        operation_type: 'cancel',
        result_status: 'success',
        result_message: { contains: '删除预约记录' },
      },
      orderBy: { timestamp: 'asc' },
      select: { appointment_id: true, timestamp: true },
    })

    let candidateIds = cancelLogs.map(l => l.appointment_id)
    const candidateMapTs = new Map()
    for (const l of cancelLogs) {
      // 相同 id 可能有多次 cancel（理论上不应），取最新一次
      candidateMapTs.set(l.appointment_id, toISODate(l.timestamp))
    }

    if (args.ids && args.ids.length > 0) {
      // 若指定只恢复某些 id
      candidateIds = candidateIds.filter(id => args.ids.includes(id))
    }

    if (candidateIds.length === 0) {
      console.log('[INFO] 未在操作日志中发现可恢复（删除）记录。')
      return
    }

    // 过滤掉当前仍然存在于 appointment_management 的 id
    const existing = await prisma.appointment_management.findMany({
      where: { id: { in: candidateIds } },
      select: { id: true },
    })
    const existingSet = new Set(existing.map(r => r.id))
    const missingIds = [...new Set(candidateIds)].filter(id => !existingSet.has(id))

    if (missingIds.length === 0) {
      console.log('[INFO] 所有 cancel 日志对应的记录目前都存在，无需恢复。')
      return
    }

    console.log(`[INFO] 发现 ${missingIds.length} 个疑似被删除且可尝试恢复的记录。`)

    // 重建每个 id 的最终快照，并准备 create 数据
    const items = []
    for (const id of missingIds) {
      const logs = await prisma.appointment_operation_log.findMany({
        where: { appointment_id: id },
        orderBy: { timestamp: 'asc' },
      })
      if (!logs || logs.length === 0) {
        console.warn(`[WARN] appointment_id=${id} 没有操作日志，跳过。`)
        continue
      }

      const snapshot = rebuildFromLogs(logs)
      if (args.onlyBooked && snapshot.status !== 'booked') {
        continue
      }
      // 可选：仅恢复含有 reference_number 的 booked 记录（更可核验）
      if (args.onlyBooked && args.bookedRequireRef && !snapshot.base?.reference_number) {
        continue
      }
      const cancelTs = candidateMapTs.get(id)
      // 从应用日志的删除行里抓原 sequence_number（时间近似匹配）
      const mappedSeq = args.generateNewSeq ? null : findNearestSequenceByTs(appDeletionLines, cancelTs, 3000) // 容忍 3 秒
      const createData = normalizeRecordForCreate(snapshot, { id, sequenceNumber: mappedSeq || undefined })
      items.push({ id, createData, hasMappedSeq: Boolean(mappedSeq) })
    }

    if (items.length === 0) {
      console.log('[INFO] 没有可恢复的明细项。')
      return
    }

    // 若启用生成新序号，则确保本批次与数据库中唯一（避免批内撞号和与历史撞号）
    if (args.generateNewSeq) {
      const usedSeq = new Set()
      // 将数据库中现有序号快速标记，按需逐条检查避免全量扫描
      for (const it of items) {
        let seq = it.createData.sequence_number
        // 重试上限，避免极端情况下死循环
        for (let attempt = 0; attempt < 50; attempt++) {
          const exists = await prisma.appointment_management.findUnique({ where: { sequence_number: seq } })
          if (!exists && !usedSeq.has(seq)) {
            usedSeq.add(seq)
            break
          }
          // 生成新的序号再试
          seq = generateSequenceNumber()
        }
        it.createData.sequence_number = [...usedSeq].pop()
      }
    }

    // 预检：检查唯一性冲突
    const conflicts = []
    for (const it of items) {
      const { sequence_number, passport_number, license_number, reference_number } = it.createData
      // 检查 seq 唯一
      const seqExists = await prisma.appointment_management.findUnique({ where: { sequence_number } })
      if (seqExists) {
        conflicts.push({ id: it.id, field: 'sequence_number', value: sequence_number })
      }
      // 检查唯一组合
      const comboExists = await prisma.appointment_management.findUnique({
        where: { passport_number_license_number: { passport_number, license_number } },
      })
      if (comboExists) {
        conflicts.push({ id: it.id, field: 'passport_number_license_number', value: `${passport_number}/${license_number}` })
      }
      // 参考编号唯一（如果存在）
      if (reference_number) {
        const refExists = await prisma.appointment_management.findUnique({ where: { reference_number } })
        if (refExists) {
          conflicts.push({ id: it.id, field: 'reference_number', value: reference_number })
        }
      }
    }

    // 汇总展示
    console.log('')
    console.log('================= Dry-run 预览 =================')
    for (const it of items) {
      const cd = it.createData
      console.log(`- id=${it.id} seq=${cd.sequence_number} (${it.hasMappedSeq ? 'from-log' : 'generated'}) name=${cd.full_name} passport=${cd.passport_number} license=${cd.license_number} status=${cd.appointment_status}`)
    }
    if (conflicts.length > 0) {
      console.log('')
      console.log('[WARN] 发现潜在唯一性冲突:')
      for (const c of conflicts) {
        console.log(`  - id=${c.id} field=${c.field} value=${c.value}`)
      }
      if (!args.apply) {
        console.log('\n[INFO] 建议处理冲突后再执行 --apply。')
      }
    }

    if (args.dryRun) {
      console.log(`\n[RESULT] 预计可恢复 ${items.length} 条记录。`)
      return
    }

    // 真正执行恢复
    const conflictedIdSet = new Set(conflicts.map(c => c.id))
    const applyItems = (conflicts.length > 0 && args.applyNonConflicting)
      ? items.filter(it => !conflictedIdSet.has(it.id))
      : items

    if (conflicts.length > 0 && !args.applyNonConflicting) {
      console.log('[ERROR] 存在唯一性冲突，已中止实际恢复。可添加 --apply-nonconflicting 仅恢复非冲突项。')
      return
    }

    let ok = 0
    let failed = 0
    for (const it of applyItems) {
      try {
        await prisma.appointment_management.create({ data: it.createData })
        ok++
      }
      catch (e) {
        failed++
        console.error(`[ERROR] 恢复 id=${it.id} 失败:`, e?.message || e)
      }
    }
    const skipped = items.length - applyItems.length
    console.log(`\n[RESULT] 恢复完成: 成功 ${ok} 条, 失败 ${failed} 条, 跳过（冲突）${skipped} 条。`)
  }
  finally {
    await prisma.$disconnect()
  }
}

main().catch((err) => {
  console.error('[FATAL] 恢复脚本异常:', err)
  process.exit(1)
})
